﻿/*
 * Simple lexer for ebnf
 */ 
using System;
using System.IO;
using System.Windows.Forms;

namespace EbnfStudio
{
    /// <summary>
    /// Simple lecical analyzer
    /// </summary>
    public class Lexer
    {

        /// <summary>
        /// Represent a lexical error
        /// </summary>
        public class LexicalError : Exception
        {
            // liner number
            public int Line { get; private set; }
            // column
            public int Column { get; private set; }
            // constructor
            public LexicalError(string msg, int line, int column) : base(msg)
            {
                Line = line; Column = column;
            }
        }


        int m_line,         // current line
            m_col,          // line column
            m_startLine,    // start of the current token
            m_startCol,     // token start poisition
            m_size;

        bool m_skipChar;    // skip reading next character
        char m_ch,          // current char
             m_nCh;         // next char

        // the string input
        StringReader m_input;


        /// <summary>
        /// Reset lexer state
        /// </summary>
        public void Reset()
        {
            m_line = 1; m_col = 0; m_startLine = 0; m_startCol = 0; m_size = 0;
            m_skipChar = false;
            m_ch = '\0'; m_nCh = '\0';
            m_input = null;
        }


        /// <summary>
        /// Lex the given input
        /// </summary>
        /// <param name="input"></param>
        public void Lex(StringReader input)
        {
            Reset();
            m_input = input;
        }


        /// <summary>
        /// Fetch next token
        /// </summary>
        public Token GetNext()
        {
            // temporary buffer
            var buffer = "";

            // iterate over the characters
            while (true)
            {
                // need to skip reading ?
                if (m_skipChar) m_skipChar = false;
                else if (!Read()) break;

                // whitespace ?
                if (char.IsWhiteSpace(m_ch)) continue;

                // token starts here
                m_startLine = m_line;
                m_startCol = m_col;
                m_size = 1;

                // comment
                if (m_ch == '(' && m_nCh == '*')
                {
                    buffer = "";
                    Read();
                    while (Read())
                    {
                        if (m_ch == '*' && m_nCh == ')')
                        {
                            Read();
                            break;
                        }
                        else buffer += m_ch;
                    }
                    return new Token(TokenClass.Comment, buffer, m_startLine, m_startCol, m_size);
                }

                // identifier ?
                if (char.IsLetter(m_ch) && m_ch != 'ε')
                {
                    var id = Identifier(false);
                    if (m_ch == '>')
                    {
                        id.ErrorCode |= TokenError.MissingIdent_Begin;
                        id.Size += 1;
                        Read();
                    }
                    return id;
                }

                // >
                if (m_ch == '>')
                {
                    var id = new Token(TokenClass.Identifier, "", m_startLine, m_startCol, 1);
                    id.Data = 1;
                    id.ErrorCode |= TokenError.MissingIdent_Begin;
                    id.ErrorCode |= TokenError.MissingIdentifier;
                    return id;
                }

                // identifier in < and > ?
                if (m_ch == '<')
                {
                    Read();

                    // read identifier
                    var id = Identifier(true);
                    id.Data = 1;
                    id.Size += 1;

                    // check length
                    if (id.Lexeme.Length == 0)
                    {
                        id.ErrorCode |= TokenError.MissingIdentifier;
                    }

                    // error ?
                    if (m_ch != '>')
                    {
                        id.ErrorCode |= TokenError.MissingIdent_End;
                    }
                    else
                    {
                        id.Size += 1;
                        Read();
                    }

                    return id;
                }

                // literal
                if (m_ch == '"' || m_ch == '\'')
                {
                    return Literal(m_ch);
                }

                // braces
                switch (m_ch)
                {
                    // { }
                    case '{': return new Token(TokenClass.Brace_Open, m_startLine, m_startCol, 1);
                    case '}': return new Token(TokenClass.Brace_Close, m_startLine, m_startCol, 1);
                    // [ ]
                    case '[': return new Token(TokenClass.Bracket_Open, m_startLine, m_startCol, 1);
                    case ']': return new Token(TokenClass.Bracket_Close, m_startLine, m_startCol, 1);
                    // ( )
                    case '(': return new Token(TokenClass.Paren_Open, m_startLine, m_startCol, 1);
                    case ')': return new Token(TokenClass.Paren_Close, m_startLine, m_startCol, 1);
                    // = | ε
                    case '=': return new Token(TokenClass.Assign, m_startLine, m_startCol, 1);
                    case '|': return new Token(TokenClass.Pipe, m_startLine, m_startCol, 1);
                    case 'ε': return new Token(TokenClass.Empty, m_startLine, m_startCol, 1);
                    // end of statement
                    case '.': case ';':
                        return new Token(TokenClass.EndOfStatement, m_startLine, m_startCol, 1);
                    // old BNF style ::= :=
                    case ':':
                        {
                            // resolve colons
                            buffer = ":"; var ln = 1;
                            while (Read() && m_ch == ':') { ln++; buffer += ':'; }
                            
                            // return invalid
                            if (m_ch != '=')
                            {
                                var tkn = new Token(TokenClass.Invalid, buffer, m_startLine, m_startCol, ln);
                                m_skipChar = true;
                                return tkn;
                            }
                            return new Token(TokenClass.Assign, buffer + "=", m_startLine, m_startCol, ln + 1);
                        }
                        
                }

                // erronous character
                buffer = "";
                buffer += m_ch;
                while (true)
                {
                    if (!Read()) break;
                    if (char.IsWhiteSpace(m_ch)) break;
                    if (char.IsLetter(m_ch)
                        || m_ch == '(' || m_ch == ')'
                        || m_ch == '[' || m_ch == ']'
                        || m_ch == '{' || m_ch == '}'
                        || m_ch == '(' || m_ch == ')'
                        || m_ch == '<' || m_ch == '>'
                        || m_ch == '|' || m_ch == 'ε'
                        || m_ch == ';' || m_ch == '.'
                        || m_ch == ':' || m_ch == '='
                        || m_ch == '"' || m_ch == '\'')
                    {
                        m_skipChar = true;
                        m_size -= 1;
                        break;
                    }
                    buffer+=m_ch;
                }
                var invtkn = new Token(TokenClass.Invalid, buffer, m_startLine, m_startCol, m_size);
                invtkn.ErrorCode |= TokenError.InvalidInput;
                return invtkn;
            }
            
            // we are at the end.
            return new Token(TokenClass.EndOfFile, m_startLine, m_startCol, 0);
        }


        /// <summary>
        /// string literal
        /// </summary>
        private Token Literal(char close)
        {
            string buffer = "";
            TokenError error = TokenError.NoError;
            Read();
            while (true)
            {
                if (m_ch == close)
                {
                    if (m_nCh != close) break;
                    Read();
                }
                else if (m_ch == '\n')
                {
                    error = TokenError.StringNotClosed;
                    break;
                }
                else if (m_ch == '\\' && (m_nCh == '\'' || m_nCh == '"' || m_ch == '\\'))
                {
                    Read();
                }
                buffer += m_ch;
                Read();
            }

            // no need to skip char!
            Token token = null;
            token = new Token(TokenClass.Literal, buffer, m_startLine, m_startCol, m_size, error);
            token.Data = (int)close;
            return token;
        }


        /// <summary>
        /// Generate identifier token
        /// </summary>
        private Token Identifier(bool allowSpaces)
        {
            // the buffer
            string lexeme = "";
            
            // get chars
            while (char.IsLetterOrDigit(m_ch) || m_ch == '_' || m_ch == '-' || (allowSpaces && m_ch == ' '))
            {
                lexeme += m_ch;
                Read();
            }

            // skip reading next one
            m_skipChar = true;

            // the token
            return new Token(TokenClass.Identifier, lexeme.Trim(), m_startLine, m_startCol, m_size - 1);
        }


        /// <summary>
        /// Read the next character from the stream
        /// </summary>
        private bool Read()
        {
            // read next char
            var ch = m_input.Read();

            // is the end ?
            if (ch == -1) { m_ch = '\n'; return false; }

            // increase size
            m_size++;

            // set the char
            m_ch = (char)ch;

            // CR + LF (win/dos) line end ?
            if (m_ch == '\r' && m_input.Peek() == '\n') return Read();
            
            // [ CR + ] LF (win or unix), CR (macos)
            if (m_ch == '\n' || m_ch == '\r')
            {
                m_line++;
                m_col = 0;
            }
            else m_col++;

            // peek
            m_nCh = (char)m_input.Peek();

            // okay!
            return true;
        }
    }
}
